from .error import *
from .records import *
from .constant import *
import collections
import itertools
import os
import pdb
import stat
import time
from . import util
from . import constant
from .util import uniquedict

import logging
logger = logging.getLogger('ctx')
del logging

def log_call(f):
    from logging import DEBUG as level
    def wrapper(self, *args, **kwargs):
        logger = self.logger
        if logger.isEnabledFor(level):
            logger.log(level, '%s(%s)',
                f.__name__,
                ', '.join(itertools.chain(
                    map(repr, args),
                    ('{}={!r}'.format(k, kwargs[k]) for k in kwargs))))
        retval = f(self, *args, **kwargs)
        logger.debug('%s = %r', f.__name__, retval)
        return retval
    return wrapper


class Context:

    logger = logger

    def __init__(self, fs):
        self._fs = fs
        self._geo = fs._geo
        self._blocks = {}
        self._inodes = {}
        self._handles = dict(fs._handles)
        self._inorefs = {}
        self._paths = {}
        self._alloc_ptr = fs._alloc_ptr
        self._time = int(time.time() * 1000000)

    def commit(self):
        for ino, inode in self._inodes.items():
            assert ino == inode['ino']
            assert ino not in self._blocks
            if self._ino_refcount(ino) > 0 or self._inode_is_linked(inode):
                self._block_put(ino, util.pad_bytes(self._geo.block_size, inode.pack(), fill=b'\xff'))
                self.logger.debug('Repacked block %i with %s', ino, inode)
            else:
                self._inode_del(inode)
        del self._inodes
        for blkno, block in self._blocks.items():
            self._fs._block_put(blkno, block)
        del self._blocks
        self.logger.debug('Committed context blocks')
        self._fs._handles = self._handles
        del self._handles
        for ino, refs in self._inorefs.items():
            if refs:
                self._fs._inorefs[ino] = refs
            else:
                del self._fs._inorefs[ino]
        del self._inorefs
        for path, info in self._paths.items():
            if info is None:
                del self._fs._paths[path]
            else:
                self._fs._paths[path] = info
        del self._paths
        self._fs._alloc_ptr = self._alloc_ptr
        del self._alloc_ptr

    def _inode_is_linked(self, inode):
        if self._inode_is_dir(inode):
            return inode['nlink'] > 1
        else:
            return inode['nlink'] > 0

    def _inode_is_dir(self, inode):
        return inode['type'] == INODE_TYPE_DIR

    def _inode_del(self, inode):
        assert not self._inode_is_linked(inode), inode
        if self._inode_is_dir(inode):
            if not self._dir_empty(inode):
                raise NotEmpty()
            assert self._inode_size(inode) == 0
        else:
            self._inode_truncate(inode, 0)
        self._block_free(inode['ino'])

    def _block_get(self, blkno):
        assert blkno not in self._inodes, blkno
        try:
            return self._blocks[blkno]
        except KeyError:
            pass
        block = self._fs._block_get(blkno)
        block = bytearray(block)
        self._blocks[blkno] = block
        return block

    def _ino_from_fh(self, fh):
        return self._handle_get(fh)

    def _ino_incref(self, ino):
        self._inorefs.setdefault(ino, self._fs._inorefs.get(ino, 0))
        self._inorefs[ino] += 1

    def _ino_decref(self, ino):
        self._inorefs.setdefault(ino, self._fs._inorefs.get(ino, 0))
        self._inorefs[ino] -= 1

    def _ino_refcount(self, ino):
        self._ino_check(ino)
        count = self._inorefs.get(ino, self._fs._inorefs.get(ino, 0))
        assert count >= 0, count
        return count

    def _ino_check(self, ino):
        if not self._geo.data_start <= ino < self._geo.data_end:
            raise ValueError('Ino is out of range: {} <= {} < {}'.format(
                self._geo.data_start, ino, self._geo.data_end))

    def _inode_size(self, inode):
        return inode['data']['size']

    def _inode_check(self, inode):
        self._ino_check(inode['ino'])
        if inode['type'] == INODE_TYPE_DIR:
            nlink_lbound = 1
        else:
            nlink_lbound = 0
        if inode['nlink'] < nlink_lbound:
            raise ValueError('Inode link count is out of range: {} <= {}'.format(nlink_lbound, inode['nlink']))

    def _inode_get(self, ino):
        self._ino_check(ino)
        if ino in self._inodes:
            return self._inodes[ino]
        inode = Inode.unpack(self._block_get(ino))
        self._inode_check(inode)
        del self._blocks[ino]
        self._inode_put(inode)
        return inode

    def _inode_put(self, inode):
        self._inodes[inode['ino']] = inode
        #del self._blocks[inode['ino']]

    def _depth_capacity(self, depth):
        if depth == 0:
            return 0
        if depth < 0:
            raise ValueError('Depth must be >= 0', depth)
        cap = self._geo.block_size
        for _ in range(depth - 1):
            cap *= self._geo.indirect_density
        return cap

    def _block_write(self, blkno, buffer, offset):
        block = self._block_get(blkno)
        block[offset:offset+len(buffer)] = buffer
        assert len(block) == self._geo.block_size, (len(block), self._geo.block_size)

    def _block_put(self, blkno, block):
        self._blocks[blkno] = bytearray(block)

    def _block_read(self, blkno, offset, count):
        return self._block_get(blkno)[offset:offset+count]

    def _data_block_write(self, blkno, buffer, offset):
        self._data_blkno_check(blkno)
        self._block_write(blkno, buffer, offset)

    def _data_block_alloc(self):
        blkno = self._block_alloc()
        self._block_put(blkno, b'\xff' * self._geo.block_size)
        return blkno

    @property
    def _indirect_struct(self):
        return self._fs._indirect_struct

    def _indirect_alloc(self):
        return self._data_block_alloc()

    def _indirect_get(self, blkno):
        return self._indirect_struct.unpack(self._data_block_get(blkno))

    def _indirect_put(self, blkno, indirect):
        self._data_block_put(blkno, indirect.pack())

    def _data_blkno_check(self, blkno):
        # TODO don't reuse ino_check
        self._ino_check(blkno)

    def _data_block_get(self, blkno):
        self._data_blkno_check(blkno)
        return self._block_get(blkno)

    def _data_block_put(self, blkno, block):
        self._data_blkno_check(blkno)
        return self._block_put(blkno, block)

    def _data_block_read(self, blkno, offset, count):
        self._data_blkno_check(blkno)
        return self._block_read(blkno, offset, count)

    @log_call
    def _data_write_recurse(self, depth, blkno, buffer, offset):
        self.logger.debug('_data_write_recurse%r', (depth, blkno, len(buffer), offset))
        if depth == 1:
            if blkno == -1:
                blkno = self._data_block_alloc()
            self._data_block_write(blkno, buffer, offset)
            return blkno
        elif depth > 1:
            if blkno == -1:
                blkno = self._data_block_alloc()
                indirect = self._indirect_struct(fill=-1)
            else:
                indirect = self._indirect_get(blkno)
            sub_capacity = self._depth_capacity(depth - 1)
            indirect_index = offset // sub_capacity
            while True:
                base_offset = sub_capacity * indirect_index
                if base_offset >= offset + len(buffer):
                    break
                sub_offset = max(offset - base_offset, 0)
                indirect[indirect_index] = self._data_write_recurse(
                    depth - 1,
                    indirect[indirect_index],
                    buffer[base_offset+sub_offset-offset:base_offset+sub_capacity-offset],
                    sub_offset)
                indirect_index += 1
            self._indirect_put(blkno, indirect)
            return blkno
        else:
            assert False, depth

    def _data_truncate_recurse(self, depth, blkno, length):
        if depth > 1:
            indirect = self._indirect_get(blkno)
            item_capacity = self._depth_capacity(depth - 1)
            for index in range(max(length // item_capacity, 0), len(indirect)):
                item_blkno = indirect[index]
                if item_blkno != -1:
                    indirect[index] = self._data_truncate_recurse(
                        depth - 1,
                        indirect[index],
                        length - index * item_capacity)
            self._indirect_put(blkno, indirect)
        if length <= 0:
            self._block_free(blkno)
            return -1
        else:
            return blkno

    def _data_read_recurse(self, depth, blkno, offset, count):
        '''Returns the buffer requested'''
        if blkno == -1:
            return bytes(count)
        if depth == 1:
            return bytes(self._data_block_get(blkno)[offset:offset+count])
        elif depth > 1:
            indirect = tuple(self._indirect_get(blkno))
            sub_capacity = self._depth_capacity(depth - 1)
            start_index = offset // sub_capacity
            end_index = (offset + count + sub_capacity - 1) // sub_capacity
            buffer = b''
            for index in range(start_index, end_index):
                sub_base = index * sub_capacity
                sub_offset = max(offset - sub_base, 0)
                sub_count = min(count + offset - sub_base, sub_capacity) - sub_offset
                sub_buffer = self._data_read_recurse(
                    depth - 1,
                    indirect[index],
                    sub_offset,
                    sub_count)
                assert len(sub_buffer) == sub_count
                buffer += sub_buffer
            return buffer
        else:
            assert False, depth

    def _data_write(self, data, buffer, offset):
        self.logger.debug('Writing %i bytes at offset %i to %s', len(buffer), offset, data)
        while offset + len(buffer) > self._depth_capacity(data['depth']):
            if data['root'] != -1:
                indirect = self._indirect_struct(fill=-1)
                indirect[0] = data['root']
                ino = self._block_alloc()
                self._indirect_put(ino, indirect)
                data['root'] = ino
            data['depth'] += 1
        self.logger.debug('After resizing, data=%s', data)
        data['root'] = self._data_write_recurse(
            data['depth'], data['root'], buffer, offset)
        data['size'] = max(data['size'], offset + len(buffer))
        self.logger.debug('After data write, %s', data)
        return len(buffer)

    def _data_truncate(self, data, length):
        if data['root'] != -1:
            data['root'] = self._data_truncate_recurse(data['depth'], data['root'], length)
        data['size'] = length
        while data['depth'] > 0 and data['size'] <= self._depth_capacity(data['depth'] - 1):
            if data['root'] != -1:
                indirect = self._indirect_get(data['root'])
                for index in range(1, len(indirect)):
                    assert indirect[index] == -1, indirect[index]
                self._block_free(data['root'])
                data['root'] = indirect[0]
            data['depth'] -= 1

    def _data_read(self, data, offset, count):
        if data['depth'] == 0:
            return b''
        if offset >= data['size']:
            return b''
        return self._data_read_recurse(
            data['depth'], data['root'], offset, min(count, data['size'] - offset))

    def _inode_write(self, inode, buffer, offset):
        retval = self._data_write(inode['data'], buffer, offset)
        if retval > 0:
            inode['ctime'] = inode['mtime'] = self._time
            if inode['type'] == INODE_TYPE_REG:
                inode['perms'] &= ~(stat.S_ISUID|stat.S_ISGID)
        self.logger.debug('After inode write, %s', inode)
        return retval

    def _inode_read(self, inode, offset, count):
        return self._data_read(inode['data'], offset, count)

    def _inode_truncate(self, inode, size):
        if self._inode_size(inode) != size:
            self._data_truncate(inode['data'], size)
            inode['ctime'] = inode['mtime'] = self._time
            if inode['type'] == INODE_TYPE_REG:
                inode['perms'] &= ~(stat.S_ISUID|stat.S_ISGID)

    def _dir_unlink(self, inode, name):
        for dirent, offset in self._dir_iter(inode):
            if dirent['name'] == name:
                break
        else:
            raise NoEntity(name)
        entry_inode = self._inode_get(dirent['ino'])
        self._inode_decref(entry_inode)
        if self._inode_is_dir(entry_inode):
            if not self._dir_empty(entry_inode):
                raise NotEmpty()
            self._inode_decref(inode)
        tempde = self._inode_read(inode, self._inode_size(inode) - Dirent.size, Dirent.size)
        self._inode_write(inode, tempde, offset)
        self._inode_truncate(inode, self._inode_size(inode) - Dirent.size)

    def _dir_add(self, dir_inode, inode, name):
        if self._dir_find(dir_inode, name):
            raise NameExists(name)
        self._inode_write(
            dir_inode,
            Dirent(name=name.encode('utf-8'), ino=inode['ino'], type=inode['type']).pack(),
            self._inode_size(dir_inode))
        if inode['type'] == INODE_TYPE_DIR:
            self._inode_incref(dir_inode)
        self._inode_incref(inode)

    def _dir_empty(self, inode):
        return self._inode_size(inode) == 0

    #def _dir_read(self, inode):
    #    pass

    def _name_check(self, name):
        if not 1 <= len(name.encode('utf-8')) <= NAME_MAX:
            raise ValueError('Invalid name length of %r: %r', len(name), name)
        if name.find('/') != -1:
            raise ValueError('Name contains directory separator: %r', name)

    def _type_check(self, type):
        for name in dir(constant):
            if name.startswith('INODE_TYPE_'):
                if type == getattr(constant, name):
                    return
        else:
            raise ValueError('Unknown inode type %o' % type)

    def _dirent_check(self, dirent):
        self._ino_check(dirent['ino'])
        self._name_check(dirent['name'])
        self._type_check(dirent['type'])

    def _dir_read(self, inode, offset):
        buf = self._inode_read(inode, offset, Dirent.size)
        if len(buf) == 0:
            return None, 0
        else:
            dirent = Dirent.unpack(buf)
            self._dirent_check(dirent)
            return dirent, offset + Dirent.size

    def _dir_iter(self, inode):
        self.logger.debug('Iterating through %s', inode)
        offset = 0
        while True:
            dirent, next_offset = self._dir_read(inode, offset)
            if next_offset == 0:
                break
            self.logger.debug('Found %s', dirent)
            yield dirent, offset
            offset = next_offset

    def _dir_find(self, inode, name):
        self.logger.debug('Finding %r in %s', name, inode)
        for dirent, *_ in self._dir_iter(inode):
            if dirent['name'] == name:
                return dirent
            self.logger.debug('%r != %r', dirent['name'], name)

    def _path_link(self, path, inode):
        parent_path, name = self._path_split(path)
        parent_inode = self._inode_from_path(parent_path)
        self._dir_add(parent_inode, inode, name)
        self._path_cache(path, {'ino': inode['ino'], 'type': inode['type']})

    def _path_unlink(self, path):
        parent_path, name = self._path_split(path)
        self._dir_unlink(self._inode_get(self._path_lookup(parent_path)['ino']), name)
        self._path_uncache(path)

    def _path_cache(self, path, dirent):
        if path not in self._paths:
            try:
                same = self._fs._paths[path]
            except KeyError:
                pass
            else:
                assert same == dirent, (same, dirent)
                return
        self._paths[path] = dirent

    def _path_uncache(self, path):
        self._paths[path] = None

    def _path_lookup_in_cache(self, path):
        try:
            return self._paths[path]
        except KeyError:
            try:
                return self._fs._paths[path]
            except KeyError:
                pass

    def _path_bootstrap(self, path):
        prefix = path
        parts = collections.deque()
        while True:
            dirent = self._path_lookup_in_cache(prefix)
            if dirent is not None:
                break
            try:
                prefix, name = prefix.rsplit('/', 1)
            except ValueError:
                raise InvalidPath(path)
            parts.appendleft(name)
            if not prefix:
                # we are looking at the root / which has no dirent
                dirent = {'ino': self._geo.data_start, 'type': INODE_TYPE_DIR}
                # destroy the trailing name, it's unavoidable when the path is /
                if len(parts) == 1 and not parts[0]:
                    parts = []
                break
        self.logger.debug('Bootstrapped %r from %r, got %s and %s left to go',
            prefix, path, dirent, parts)
        return dirent, parts

    def _path_lookup(self, path):
        dirent, parts = self._path_bootstrap(path)
        for name in parts:
            if not name:
                raise InvalidPath(path)
            if len(name.encode('utf-8')) > NAME_MAX:
                raise NameTooLong(name)
            dirent = self._dir_find(self._inode_get(dirent['ino']), name)
            if dirent is None:
                raise NoEntity(path)
        self.logger.debug('Found path %r: %s', path, dirent)
        self._path_cache(path, dirent)
        return dirent

    def _block_alloc(self):
        return self._bitmap_alloc()

    def _block_free(self, blkno):
        self._data_blkno_check(blkno)
        try:
            del self._blocks[blkno]
        except KeyError:
            pass
        return self._bitmap_free(blkno)

    def _bitmap_locate(self, index):
        '''Takes a bitmap index and returns
        the blkno of the bitmap block, the byte index, and its bit index in the bitmap'''
        block, block_bit = divmod(index, self._geo.bitmap_density)
        byte, bit = divmod(block_bit, 8)
        return block + self._geo.bitmap_start, byte, bit

    def _bitmap_alloc(self):
        self.logger.debug('Starting allocation routine at bitmap index %r', self._alloc_ptr)
        for skip in range(1, self._geo.data_blocks + 1):
            bitmap_index = (self._alloc_ptr + skip) % self._geo.data_blocks
            bitmap_blkno, byte_index, bit_index = self._bitmap_locate(bitmap_index)
            block = self._block_get(bitmap_blkno)
            if not (block[byte_index] >> bit_index) & 1:
                block[byte_index] |= 1 << bit_index
                self._alloc_ptr = bitmap_index
                blkno = bitmap_index + self._geo.data_start
                self.logger.debug('Allocated block %r', blkno)
                return blkno
        else:
            raise NoSpace()

    def _bitmap_free(self, blkno):
        bitmap_blkno, byte_index, bit_index = self._bitmap_locate(blkno - self._geo.data_start)
        bitmap_block = self._block_get(bitmap_blkno)
        assert bitmap_block[byte_index] & 1 << bit_index, (bin(bitmap_block[byte_index]), bit_index)
        bitmap_block[byte_index] &= ~(1 << bit_index)
        self.logger.debug('Freed block %r', blkno)

    def _inode_alloc(self):
        return self._block_alloc()

    def _inode_new(self, *, type, perms, rdev, uid, gid):
        t = self._time
        inode = Inode(
            ino=self._inode_alloc(),
            nlink=(1 if type == INODE_TYPE_DIR else 0),
            type=type, perms=perms, rdev=0,
            mtime=t, ctime=t, atime=t,
            data=Data(root=-1, depth=0, size=0),
            uid=uid, gid=gid,)
        self._inode_put(inode)
        return inode

    def _inode_incref(self, inode):
        inode['nlink'] += 1
        inode['ctime'] = self._time

    def _inode_decref(self, inode):
        inode['nlink'] -= 1
        inode['ctime'] = self._time
        self.logger.debug('Decremented link count: %s', inode)

    def _handle_new(self, ino):
        for i in itertools.count():
            if i not in self._handles:
                self._handles[i] = ino
                self._ino_incref(ino)
                return i

    def _handle_del(self, fh):
        ino = self._handles.pop(fh)
        self._ino_decref(ino)

    def _handle_get(self, fh):
        try:
            return self._handles[fh]
        except KeyError as exc:
            assert exc.args[0] == fh
            raise BadFileHandle(fh)

    def chmod(self, path, perms):
        inode = self._inode_from_path(path)
        inode['perms'] = perms
        inode['ctime'] = self._time

    def chown(self, path, uid, gid):
        inode = self._inode_from_path(path)
        if uid == -1 and gid == -1:
            return
        if uid != -1:
            inode['uid'] = uid
        if gid != -1:
            inode['gid'] = gid
        inode['ctime'] = self._time
        inode['perms'] &= ~(stat.S_ISUID|stat.S_ISGID)

    @log_call
    def stat(self, path):
        return self._inode_from_path(path)

    @log_call
    def fstat(self, fh):
        return self._inode_get(self._ino_from_fh(fh))

    def utimes(self, path, access=None, modify=None):
        inode = self._inode_from_path(path)
        if access is not None:
            inode['atime'] = int(access * 1000000)
        if modify is not None:
            inode['mtime'] = int(modify * 1000000)

    @log_call
    def mkdir(self, path, perms, uid, gid):
        self._path_link(path, self._inode_new(
            type=INODE_TYPE_DIR, perms=perms, uid=uid, gid=gid, rdev=0))

    @log_call
    def remove(self, path):
        self._path_unlink(path)

    @log_call
    def opendir(self, path):
        de = self._path_lookup(path)
        if de['type'] != INODE_TYPE_DIR:
            raise NotDirectory(path)
        return self._handle_new(de['ino'])

    @log_call
    def readdir(self, fh, offset):
        ino = self._ino_from_fh(fh)
        if offset == 0:
            return '.', Inode(ino=ino, type=INODE_TYPE_DIR), -2
        elif offset == -2:
            return '..', Inode(ino=-1, type=INODE_TYPE_DIR), -1
        elif offset == -1:
            offset = 0
        inode = self._inode_get(ino)
        dirent, offset = self._dir_read(inode, offset)
        if offset:
            return (
                dirent['name'],
                Inode(ino=dirent['ino'], type=dirent['type']),
                offset)
        else:
            return None, None, 0

    def _path_split(self, path):
        head, tail = path.rsplit('/', 1)
        if not head:
            head = '/'
        return head, tail

    @log_call
    def mknod(self, path, type, perms, rdev, uid, gid):
        self._path_link(path, self._inode_new(
            type=type,
            perms=perms,
            uid=uid,
            gid=gid,
            rdev=rdev))

    def _inode_from_path(self, path):
        return self._inode_get(self._path_lookup(path)['ino'])

    @log_call
    def open(self, path, uid, gid, type=None, perms=None,
            *, create=False):
        parent_path, name = self._path_split(path)
        parent_inode = self._inode_from_path(parent_path)
        dirent = self._dir_find(parent_inode, name)
        if dirent is None:
            if create:
                inode = self._inode_new(
                    type=type,
                    perms=perms,
                    uid=uid,
                    gid=gid,
                    rdev=0)
                self._dir_add(parent_inode, inode, name)
                self._path_cache(path, inode)
            else:
                raise NoEntity(path)
        else:
            inode = self._inode_get(dirent['ino'])
        if inode['type'] == INODE_TYPE_DIR:
            raise IsADirectory(path)
        return self._handle_new(inode['ino'])

    @log_call
    def close(self, fh):
        self._handle_del(fh)

    @log_call
    def releasedir(self, fh):
        self._handle_del(fh)

    def symlink(self, target, path, uid, gid):
        inode = self._inode_new(
            type=INODE_TYPE_LNK,
            perms=0o777,
            uid=uid,
            gid=gid,
            rdev=0,)
        self._path_link(path, inode)
        self._inode_write(inode, target, 0)

    def link(self, target, path):
        inode = self._inode_from_path(target)
        self._path_link(path, inode)

    def readlink(self, path, size):
        return self._inode_read(self._inode_from_path(path), 0, size)

    def read(self, fh, nbyte, offset):
        return self._inode_read(self._inode_get(self._ino_from_fh(fh)), offset, nbyte)

    def write(self, fh, buf, offset):
        return self._inode_write(self._inode_get(self._ino_from_fh(fh)), buf, offset)

    def ftruncate(self, fh, length):
        self._inode_truncate(self._inode_get(self._ino_from_fh(fh)), length)

    def truncate(self, path, length):
        self._inode_truncate(self._inode_from_path(path), length)

    def rename(self, oldpath, newpath):
        inode = self._inode_from_path(oldpath)
        self._path_unlink(oldpath)
        try:
            inode2 = self._inode_from_path(newpath)
        except NoEntity as exc:
            if exc.args[0] != newpath:
                raise
        else:
            if self._inode_is_dir(inode):
                if not self._inode_is_dir(inode2):
                    raise NotDirectory(newpath)
            else:
                if self._inode_is_dir(inode2):
                    raise IsDirectory(newpath)
            self._path_unlink(newpath)
        self._path_link(newpath, inode)
